package com.clover.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁
 * redis是单进程单线程模式，当外部并发访问redis时，redis会将请求放入任务队列，顺序执行，所以对于redis不存在并发情况，利用此特性做分布式锁
 * Created by sanyecao on 2017/10/17.
 */
@Component
public class RedisLockHelper {
    private static final Logger logger = LoggerFactory.getLogger(RedisLockHelper.class);

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final int MAX_RETIES = 3; //重试次数

    private final long TRY_SLEEP_MILLISECONDS = 10;//重试间隔，单位：毫秒

    private final long TIME_OUT = 2000; //有效时间，单位：毫秒


    /**
     * 获取锁，并可以重试获取
     * @param key
     * @param timeout 有效时间
     * @return
     */
    public Boolean lockAndRetry(String key,long timeout){
        //重试次数
        int tryTime = 0;

        //获取锁结果
        Boolean isLock = Boolean.FALSE;

        //循环获取
        while(tryTime < MAX_RETIES){
            //获取锁
            isLock = lock(key,timeout);

            //如果获取到，则返回
            if(isLock){
                logger.info("获取redis锁重试次数：{}",tryTime);
                return isLock;
            }

            //线程等待TRY_SLEEP_MILLISECONDS毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(TRY_SLEEP_MILLISECONDS);
            } catch (InterruptedException e) {
                logger.debug("获取redis锁[" + key + "]休眠被打断!", e);
            }

            //重试次数加1
            tryTime ++;

            //继续重试
            continue;
        }

        //最大重试后也无法获取，则返回失败
        logger.info("获取redis锁重试次数：{}",tryTime);
        return isLock;
    }

    /**
     * 获取锁，可以重试
     * @param key
     * @return
     */
    public Boolean lockAndRetry(String key){
        return lockAndRetry(key,TIME_OUT);
    }

    /**
     * 获取锁，不重试
     * @param key
     * @return
     */
    public Boolean lock(String key){
        return lock(key ,TIME_OUT);
    }

    /**
     * 获取锁
     * 逻辑：
     *  1.通过setnx（key,unixTime+timeout）方法设置锁的有效期，如果返回1，表示设置成功，即成功获取了锁 ；如果返回值为0，则往下进行判断
     *  2.通过get（key）方法，获取步骤1设置的过期时间expireTime，如果当前unixTime <= expireTime,则说明其他线程设置的锁有效，则此次获取锁失败 ；如果unixTime > expireTime,则说明锁已失效，可继续往下进行判断
     *  3.通过getset（key,unixTime+timeout）方法返回旧的过期时间expireTime2，如果expireTime = expireTime2，则说明这个过程中，没有其他线程设置锁，则此次获取锁成功；反则，此次获取锁失败
     *
     * 耗时分析：
     *  获取连接和释放连接耗时较多，lock总耗时本地测试在80毫秒左右，内部redis操作基本在1，2毫秒，其余耗时为获取连接与释放连接
     * @param key 锁定的键
     * @param timeout 有效时间(单位：毫秒)
     * @return
     */
    public Boolean lock(String key ,long timeout){
        return redisTemplate.execute((RedisConnection redisConnection) -> {
            long startTime = System.currentTimeMillis();

            long redisTime =  redisConnection.time();
            logger.info("当前系统时间:{};redis服务器时间:{}",startTime,redisTime);

            byte[] redisKey = redisTemplate.getStringSerializer().serialize(key);
            byte[] redisValue = redisTemplate.getStringSerializer().serialize(String.valueOf(redisTime + timeout));

            //1.通过setnx方法，设置锁有效期
            boolean setNxRes = redisConnection.setNX(redisKey,redisValue);

            //1.1 如果设置成功，则获取到了锁
            if(setNxRes){
                logger.info("到达步骤1-获取锁耗时:{}",System.currentTimeMillis()-startTime);
                return true;
            }

            //2.通过get方法，获取过期时间
            byte[] oldExpireTimeByte =  redisConnection.get(redisKey);

            //2.1 判断有效期是否失效,如果有效期还有效，则获取锁失败
            String oldExpireTimeStr = redisTemplate.getStringSerializer().deserialize(oldExpireTimeByte);
            if(System.currentTimeMillis() <= Long.parseLong(oldExpireTimeStr)){
                logger.info("到达步骤2-获取锁耗时:{}",System.currentTimeMillis()-startTime);
                return false;
            }

            //3.通过getset方法设置新的有效期
//            byte[] redisValue2 = redisTemplate.getStringSerializer().serialize(String.valueOf(System.currentTimeMillis() + timeout));
            byte[] oldExpireTimeByte2 = redisConnection.getSet(redisKey,redisValue);

            //3.1 判断中间是否有别的线程设置过过期时间
            String oldExpireTimeStr2 = redisTemplate.getStringSerializer().deserialize(oldExpireTimeByte2);
            if(oldExpireTimeStr.equals(oldExpireTimeStr2)){
                logger.info("到达步骤3-获取锁耗时:{}",System.currentTimeMillis()-startTime);
                return true;
            }

            //3.2 如果期间别的线程设置过过期时间，则获取锁失败
            logger.info("到达步骤3-获取锁耗时:{}",System.currentTimeMillis()-startTime);
            return false;
        });
    }

    /**
     * 释放锁
     * @param key
     */
    public boolean release(String key){
        return redisTemplate.execute((RedisConnection redisConnection) -> {
            long startTime = System.currentTimeMillis();
            byte[] redisKey = redisTemplate.getStringSerializer().serialize(key);

            long num = redisConnection.del(redisKey);

            logger.info("释放锁结果:{}，耗时:{}",num,System.currentTimeMillis()-startTime);
            return true;
        });
    }

}
